/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <journal/app/constants.h>
#include <journal/views/ink_surface.h>
#include <journal/views/interfaces.h>
#include <journal/models/interfaces.h>
#include <journal/common/util.h>

namespace journal { namespace view {

namespace bi = baja::ink;

ink_surface::ink_surface(
    const std::shared_ptr<model::iink_surface_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    ) : 
    _state(state::idle),
    _inkSurfaceModel(model),
    _inputCapture(inputCapture),
    _blockWatermark(-1),
    _bundleWatermark(0),
    _rebuildCache(true),
    _surfaceObserverCookie(-1),
    _currentTool(ink_surface_tool::drawing_tool)
{
    _generatedInk = bi::createInkData(std::make_shared<ink_configuration>());
}

std::shared_ptr<ink_surface> ink_surface::create(
    const std::shared_ptr<model::iink_surface_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<ink_surface> returnValue = std::make_shared<ink_surface>(model, inputCapture);
    returnValue->init();
    return returnValue;
}

void ink_surface::init()
{
    // observe the model
    _surfaceObserverCookie = _inkSurfaceModel->addObserver(
        std::dynamic_pointer_cast<model::iink_surface_model_observer>(shared_from_this()));
}

void ink_surface::onRenderDeferred()
{
    (void)this->renderToCacheInternal(true); // deferred
}

bool ink_surface::renderToCache()
{
    return this->renderToCacheInternal(false); // not deferred
}

void ink_surface::renderToIdsCache()
{
    std::shared_ptr<ink_configuration> configuration = std::make_shared<ink_configuration>();
    std::shared_ptr<bi::iink_renderer> strokeInkRenderer = bi::createInkRenderer(configuration, constants::graphics());
    strokeInkRenderer->setInkData(_generatedInk);
    strokeInkRenderer->render(
        constants::graphics(),
        constants::general::pageWorldDimensions(),
        common::fullPageViewport(),
        0,      // start at bundle 0
        0,      // start at block 0
        false,  // no need for deferral
        true    // ids mode
        );

    _inkIdsMap = strokeInkRenderer->populateIdsMap(
        static_cast<b::uint32>(constants::general::pagePixelDimensions().x),
        static_cast<b::uint32>(constants::general::pagePixelDimensions().y),
        true);
}

bool ink_surface::renderToCacheInternal(const bool deferred)
{
    bool returnValue = false;
    b::uint32 blockStartIndex = _blockWatermark + 1;
    b::uint32 bundleStartIndex = _bundleWatermark;

    b::Boolean catchUp = false;
    if (0 != _generatedInk->bundleCount())
    {
        if (bundleStartIndex < _generatedInk->bundleCount() - 1)
        {
            catchUp = true;
        }
        else if (bundleStartIndex == _generatedInk->bundleCount() - 1)
        {
            if (blockStartIndex < _generatedInk->blockCount(bundleStartIndex))
            {
                catchUp = true;
            }
        }
    }

    if (catchUp)
    {
        std::shared_ptr<ink_configuration> configuration = std::make_shared<ink_configuration>();
        std::shared_ptr<bi::iink_renderer> strokeInkRenderer = bi::createInkRenderer(configuration, constants::graphics());
        strokeInkRenderer->setInkData(_generatedInk);
        strokeInkRenderer->render(
            constants::graphics(),
            constants::general::pageWorldDimensions(),
            common::fullPageViewport(),
            bundleStartIndex,
            blockStartIndex,
            deferred,
            false
            );

        // update water mark...
        _bundleWatermark = static_cast<b::int32>(_generatedInk->bundleCount()) - 1;
        _blockWatermark = static_cast<b::int32>(_generatedInk->blockCount(_bundleWatermark)) - 1;
        returnValue = true;
    }

    this->setRebuildCache(false);
    return returnValue;
}

void ink_surface::selectTool(ink_surface_tool tool)
{
    if (_currentTool != tool)
    {
        if (tool == ink_surface_tool::eraser_tool)
        {
            renderToIdsCache();
        }
    }
    _currentTool = tool;
}

bool ink_surface::getDynamicBounds(m::rectangle* bounds)
{
    std::shared_ptr<bi::iink_renderer> dynamicStrokeInkRenderer = _dynamicStrokeInkRenderer;
    if (_currentStrokeModel && _currentStrokeModel->points()->size() && dynamicStrokeInkRenderer)
    {
        *bounds = dynamicStrokeInkRenderer->bounds();
        return true;
    }

    return false;
}

void ink_surface::renderDynamic()
{
    std::shared_ptr<bi::iink_renderer> dynamicStrokeInkRenderer = _dynamicStrokeInkRenderer;
    if (_currentStrokeModel && dynamicStrokeInkRenderer)
    {
        std::shared_ptr<bi::iink_stroke_data> modelData = std::dynamic_pointer_cast<bi::iink_stroke_data>(_currentStrokeModel);
        dynamicStrokeInkRenderer->generateDynamicStrokeVertices(modelData);
        dynamicStrokeInkRenderer->render(
            constants::graphics(),
            constants::general::pageWorldDimensions(),
            common::fullPageViewport(),
            0,
            0,
            false /* deferred = false */,
            false /* ids mode = false*/);
    }
}

void ink_surface::removeInkStroke(b::uint32 strokeId)
{
    _inkSurfaceModel->removeInkStroke(strokeId);
}

void ink_surface::localToIdsMap(
    const page_pointer_context& ppc,
    b::uint32& x,
    b::uint32& y)
{
    b::float32 pageWidthInPixels = constants::general::pagePixelDimensions().x;
    b::float32 pageHeightInPixels = constants::general::pagePixelDimensions().y;

    b::float32 w = constants::general::pageWorldDimensions().x;
    b::float32 h = constants::general::pageWorldDimensions().y;

    b::float32 x0 = w / 2 * -1;
    b::float32 y0 = h / 2;

    x = static_cast<b::uint32>(( ppc.localPoint.x - x0 ) * pageWidthInPixels / w);
    y = static_cast<b::uint32>(( ppc.localPoint.y - y0 ) * pageHeightInPixels / h * -1);
}

bool ink_surface::detectAndRemoveStrokes(const page_pointer_context& ppc)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_INK_DELETE, L"ink_surface::detectStrokes() : entering");

    b::uint32 eraserHalfWidth = constants::ink::eraserSizeInPixels() / 2;

    // map local coordinates to ids map
    b::uint32 px = 0;
    b::uint32 py = 0;
    localToIdsMap(ppc, px, py);

    // find ids...
    std::map<b::uint32, bool> ids;
    _inkIdsMap->findIds(
        px - eraserHalfWidth,
        py - eraserHalfWidth,
        px + eraserHalfWidth,
        py + eraserHalfWidth,
        ids);

    // remove ids...
    std::map<b::uint32, bool>::iterator strokeIdIt;
    for (strokeIdIt = ids.begin() ; strokeIdIt != ids.end() ; ++strokeIdIt)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_INK_DELETE,
            boost::wformat(L"ink_surface::detectStrokes() : deleting %i") %
            (*strokeIdIt).first
            );

        this->removeInkStroke((*strokeIdIt).first);
    }

    return ids.size() != 0;
}

void ink_surface::onPointer(const a::pointer_args& args, void* context)
{
    page_pointer_context* ppc = static_cast<page_pointer_context*>(context);
    ppc->handled = false;

    // only support drawing with pen and mouse (no keys pressed)
    bool isInkEvent = 
        args.deviceType == a::device_type::pen ||
        (args.deviceType == a::device_type::mouse && !static_cast<b::int32>(args.keys) ||
        (args.deviceType == a::device_type::touch && constants::ink::touchInputMode())) && 
        (constants::shellService()->touchPointCount() <= 1);

    if (!isInkEvent)
        return;

    // TRACK: hack bug 362.
    bool toolSwitched = false;
    if ((args.eraser == true || constants::ink::inkTool() == constants::ink::ink_tool::eraser))
    {
        if (_currentTool != ink_surface_tool::eraser_tool)
        {
            // the eraser event doesn't get signaled from the beginning.
            // so, we might enter the drawing state, and then get the eraser event.
            // in that case, we end the current drawing (which shouldn't have taken place
            // in the first place).
            if (_state == state::drawing)
            {
                toolSwitched = true;
                _state = state::idle;
                this->finishCurrentStroke();
                this->raiseDirty(); // TRACK: PERF: we may be dirtying up more than necessary for example of you just drop one single dot we'll dirty on up down and a moot move
             }

            selectTool(ink_surface_tool::eraser_tool);
        }
    }
    else
    {
        selectTool(ink_surface_tool::drawing_tool);
    }

    // calculate state transition
    switch (_state)
    {
        case state::idle:
            // the pen is put down on the page
            if ((args.type == a::pointer_event_type::down || toolSwitched) && ppc->isHit)
            {
                // grab the capture so we can handle this from now on until the pen is back up
                _inputCapture.lock()->setCapture(std::static_pointer_cast<a::iinput_observer>(shared_from_this()));

                // TRACK: 78
                // model::createInkStroke(_inkSurfaceModel);

                if (_currentTool == ink_surface_tool::eraser_tool)
                {
                    if (true == detectAndRemoveStrokes(*ppc))
                    {
                        this->raiseDirty();
                    }

                    _state = state::deleting;
                }
                else
                {
                    _currentStrokeModel = _inkSurfaceModel->createInkStroke();
                    _currentStrokeModel->addPoint(m::point2d(ppc->localPoint.x, ppc->localPoint.y), args.pressure, static_cast<b::uint64>(constants::general::currentTime()));

                    // set the stroke properties from the current settings
                    _currentStrokeModel->setColor(static_cast<bi::ink_color>(constants::ink::inkColor()));
                    _currentStrokeModel->setWidth(constants::ink::baseWidth());

                    // create a new renderer and add the point we just added
                    std::shared_ptr<ink_configuration> configuration = std::make_shared<ink_configuration>();
                    std::shared_ptr<bi::iink_renderer> dynamicStrokeInkRenderer = bi::createInkRenderer(configuration, constants::graphics());
                    std::shared_ptr<bi::iink_stroke_data> modelData = std::dynamic_pointer_cast<bi::iink_stroke_data>(_currentStrokeModel);
                    dynamicStrokeInkRenderer->setDynamicStrokeModel(modelData);   // this needs to happen before _dynamicStrokeInkRenderer
                                                                                            // is set or else the onRender() might access a non-initialized
                                                                                            // renderer.
                    _dynamicStrokeInkRenderer = dynamicStrokeInkRenderer;

                    _state = state::drawing;

                    this->raiseDirty();
                }
                _currentDeviceType = args.deviceType;

                ppc->handled = true;
            }
            break;

        case state::drawing:
            if (args.deviceType != _currentDeviceType)
            {
                // ignore it if we were drawing and we get an even from a different device
                return;
            }

            if (args.type == a::pointer_event_type::move)
            {
                if (ppc->isHit)
                {
                    // update stroke only if we are hit
                    _currentStrokeModel->addPoint(m::point2d(ppc->localPoint.x, ppc->localPoint.y), args.pressure, static_cast<b::uint64>(constants::general::currentTime()));
                    this->raiseDirty();
                    ppc->handled = true;
                }
            }
            else if (args.type == a::pointer_event_type::up)
            {
                // released the button, so stop drawing
                _state = state::idle;
                this->finishCurrentStroke();
                this->raiseDirty(); // TRACK: PERF: we may be dirtying up more than necessary for example of you just drop one single dot we'll dirty on up down and a moot move
                ppc->handled = true;
            }
            break;
        case state::deleting:
            if (args.deviceType != _currentDeviceType)
            {
                // ignore it if we were drawing and we get an even from a different device
                return;
            }

            if (args.type == a::pointer_event_type::move)
            {
                if (ppc->isHit)
                {
                    if (true == detectAndRemoveStrokes(*ppc))
                    {
                        this->raiseDirty();
                    }
                    ppc->handled = true;
                }
            }
            else if (args.type == a::pointer_event_type::up)
            {
                // released the button, so stop drawing
                _state = state::idle;
                _inputCapture.lock()->releaseCapture();
                ppc->handled = true;
            }
            break;
        }
}

void ink_surface::finishCurrentStroke()
{
    if (_currentStrokeModel)
    {
        // raise stroke added event
        _observerSource.raise(
            [&](const std::shared_ptr<view::iink_surface_observer>& observer, void* context)
            {
                observer->onStrokeAdded(_currentStrokeModel);
            });

        _generatedInk->append(_dynamicStrokeInkRenderer->inkData());
        _dynamicStrokeInkRenderer = nullptr;
        _inputCapture.lock()->releaseCapture();
        _currentStrokeModel = nullptr;
    }
}

void ink_surface::generateInk(const std::shared_ptr<model::iink_stroke_model>& newStroke)
{
    ACQUIRE_VIEW_LOCK;

    std::shared_ptr<bi::iink_stroke_data> modelData = std::dynamic_pointer_cast<bi::iink_stroke_data>(newStroke);
    std::shared_ptr<ink_configuration> configuration = std::make_shared<ink_configuration>();
    std::shared_ptr<bi::iink_renderer> strokeInkRenderer = bi::createInkRenderer(configuration, constants::graphics());
    strokeInkRenderer->setDynamicStrokeModel(modelData);
    strokeInkRenderer->generateDynamicStrokeVertices(modelData);
    _generatedInk->append(strokeInkRenderer->inkData());
    raiseDirty();
}

void ink_surface::onInkSurfaceModelReady(
    const std::shared_ptr<model::iink_surface_model>& inkSurface)
{
    ACQUIRE_VIEW_LOCK;
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** ink_surface::onInkSurfaceModelReady() : inkSurfaceId = %i ...") %
        inkSurface->storageId()
        );
}

void ink_surface::onStrokeModelAttached(const std::shared_ptr<model::iink_stroke_model>& newStroke)
{
    ACQUIRE_VIEW_LOCK;
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** ink_surface::onStrokeModelAttached() : inkStrokeId = %i ...") %
        newStroke->storageId()
        );
    // noop
    // NOTE: when a stroke is erased, we can invalidate the cache right away from here
    this->raiseDirty();
}

void ink_surface::onStrokeModelReady(const std::shared_ptr<model::iink_stroke_model>& newStroke)
{
    ACQUIRE_VIEW_LOCK;
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** ink_surface::onStrokeModelReady() : inkStrokeId = %i ...") %
        newStroke->storageId()
        );
}

void ink_surface::onStrokeModelRemoved(
    const std::shared_ptr<model::iink_stroke_model>& stroke)
{
    ACQUIRE_VIEW_LOCK;
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** ink_surface::onStrokeModelRemoved() : inkStrokeId = %i ...") %
        stroke->storageId()
        );
    // NOTE: when a stroke is erased, we can invalidate the cache right away from here
    this->setRebuildCache(true);
    this->raiseDirty();
}

void ink_surface::onInkSurfaceChildrenReady(
        const std::shared_ptr<model::iink_surface_model>& inkSurface)
{
    ACQUIRE_VIEW_LOCK;
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** ink_surface::onInkSurfaceChildrenReady() : inkSurfaceId = %i ...") %
        inkSurface->storageId()
        );

    regenerateInk();
}

void ink_surface::regenerateInk()
{
    std::shared_ptr<ink_configuration> configuration = std::make_shared<ink_configuration>();
    std::shared_ptr<bi::iink_renderer> strokeInkRenderer = bi::createInkRenderer(configuration, constants::graphics());
    strokeInkRenderer->resetStrokes();
    b::uint32 strokeCount = static_cast<b::uint32>(_inkSurfaceModel->inkStrokes()->size());
    for (b::uint32 i = 0; i < strokeCount; i++)
    {
        std::shared_ptr<bi::iink_stroke_data> inkStrokeData = std::dynamic_pointer_cast<bi::iink_stroke_data>(_inkSurfaceModel->inkStrokes()->item(i));
        strokeInkRenderer->generateStaticStrokeVertices(inkStrokeData);
    }
    _generatedInk = strokeInkRenderer->inkData();
}

void ink_surface::rebuildRenderData()
{
    regenerateInk();
}

void ink_surface::cleanup()
{
    if (!_inputCapture.expired())
        _inputCapture.lock()->releaseCapture();
    if (_inkSurfaceModel) _inkSurfaceModel->removeObserver(_surfaceObserverCookie);
    _surfaceObserverCookie = -1;
    _dynamicStrokeInkRenderer = nullptr;
}

std::shared_ptr<iink_surface> createInkSurface(
    const std::shared_ptr<model::iink_surface_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<iink_surface> returnValue = ink_surface::create(model, inputCapture);
    return std::static_pointer_cast<iink_surface>(returnValue);
}

}} // namespace journal::view